/*
 ===========================================================================
 Copyright (C) 1999-2005 Id Software, Inc.
 Copyright (C) 2000-2006 Tim Angus

 This file is part of Tremulous.

 Tremulous is free software; you can redistribute it
 and/or modify it under the terms of the GNU General Public License as
 published by the Free Software Foundation; either version 2 of the License,
 or (at your option) any later version.

 Tremulous is distributed in the hope that it will be
 useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.

 You should have received a copy of the GNU General Public License
 along with Tremulous; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 ===========================================================================
 */

// cg_players.c -- handle the media and animation for player entities


#include "cg_local.h"
//FIX ME, seems to try load this sounds per each model gah.
char *cg_customSoundNames[MAX_CUSTOM_SOUNDS] =
{ "*death1.wav", "*death2.wav", "*death3.wav", "*death1zombie.wav", "*death2zombie.wav", "*death3zombie.wav", "*jump1.wav", "*pain25_1.wav", "*pain50_1.wav",
    "*pain75_1.wav", "*pain100_1.wav", "*pain25_1zombie.wav", "*pain50_1zombie.wav", "*pain75_1zombie.wav", "*pain100_1zombie.wav", "*falling1.wav",
    "*gasp.wav", "*drown.wav", "*fall1.wav", "*fall1zombie.wav", "*tauntzombie.wav", "*taunt.wav" };

/*
 ================
 CG_CustomSound

 ================
 */
sfxHandle_t
CG_CustomSound(int clientNum, const char *soundName)
{
  clientInfo_t *ci;
  int i;

  if (soundName[0] != '*')
    return trap_S_RegisterSound(soundName, qfalse);

  if (clientNum < 0 || clientNum >= MAX_CLIENTS)
    clientNum = 0;

  ci = &cgs.clientinfo[clientNum];

  for(i = 0;i < MAX_CUSTOM_SOUNDS && cg_customSoundNames[i];i++)
  {
    if (!strcmp(soundName, cg_customSoundNames[i]))
      return ci->sounds[i];
  }

  CG_Error("Unknown custom sound: %s", soundName);
  return 0;
}

/*
 =============================================================================

 CLIENT INFO

 =============================================================================
 */

/*
 ======================
 CG_ParseAnimationFile

 Read a configuration file containing animation coutns and rates
 models/players/visor/animation.cfg, etc
 ======================
 */
static qboolean
CG_ParseAnimationFile(const char *filename, clientInfo_t *ci)
{
  char *text_p, *prev;
  int len;
  int i;
  char *token;
  float fps;
  int skip;
  char text[20000];
  fileHandle_t f;
  animation_t *animations;

  animations = ci->animations;

  // load the file
  len = trap_FS_FOpenFile(filename, &f, FS_READ);
  if (len <= 0)
    return qfalse;

  if (len >= sizeof(text) - 1)
  {
    CG_Printf("File %s too long\n", filename);
    trap_FS_FCloseFile(f);
    return qfalse;
  }

  trap_FS_Read(text, len, f);
  text[len] = 0;
  trap_FS_FCloseFile(f);

  // parse the text
  text_p = text;
  skip = 0; // quite the compiler warning

  ci->footsteps = FOOTSTEP_NORMAL;
  VectorClear( ci->headOffset );
  ci->gender = GENDER_MALE;
  ci->fixedlegs = qfalse;
  ci->fixedtorso = qfalse;
  ci->nonsegmented = qfalse;

  // read optional parameters
  while(1)
  {
    prev = text_p; // so we can unget
    token = COM_Parse(&text_p);

    if (!token)
      break;

    if (!Q_stricmp(token, "footsteps"))
    {
      token = COM_Parse(&text_p);
      if (!token)
        break;

      if (!Q_stricmp(token, "default") || !Q_stricmp(token, "normal"))
        ci->footsteps = FOOTSTEP_NORMAL;
      else if (!Q_stricmp(token, "flesh"))
        ci->footsteps = FOOTSTEP_FLESH;
      else if (!Q_stricmp(token, "none"))
        ci->footsteps = FOOTSTEP_NONE;
      else if (!Q_stricmp(token, "custom"))
        ci->footsteps = FOOTSTEP_CUSTOM;
      else
        CG_Printf("Bad footsteps parm in %s: %s\n", filename, token);

      continue;
    }
    else if (!Q_stricmp(token, "headoffset"))
    {
      for(i = 0;i < 3;i++)
      {
        token = COM_Parse(&text_p);
        if (!token)
          break;

        ci->headOffset[i] = atof(token);
      }

      continue;
    }
    else if (!Q_stricmp(token, "sex"))
    {
      token = COM_Parse(&text_p);

      if (!token)
        break;

      if (token[0] == 'f' || token[0] == 'F')
        ci->gender = GENDER_FEMALE;
      else if (token[0] == 'n' || token[0] == 'N')
        ci->gender = GENDER_NEUTER;
      else
        ci->gender = GENDER_MALE;

      continue;
    }
    else if (!Q_stricmp(token, "fixedlegs"))
    {
      ci->fixedlegs = qtrue;
      continue;
    }
    else if (!Q_stricmp(token, "fixedtorso"))
    {
      ci->fixedtorso = qtrue;
      continue;
    }
    else if (!Q_stricmp(token, "nonsegmented"))
    {
      ci->nonsegmented = qtrue;
      continue;
    }

    // if it is a number, start parsing animations
    if (token[0] >= '0' && token[0] <= '9')
    {
      text_p = prev; // unget the token
      break;
    }

    Com_Printf("unknown token '%s' is %s\n", token, filename);
  }

  if (!ci->nonsegmented)
  {
    // read information for each frame
    for(i = 0;i < MAX_PLAYER_ANIMATIONS;i++)
    {
      token = COM_Parse(&text_p);

      if (!*token)
      {
        if (i >= TORSO_GETFLAG && i <= TORSO_NEGATIVE)
        {
          animations[i].firstFrame = animations[TORSO_GESTURE].firstFrame;
          animations[i].frameLerp = animations[TORSO_GESTURE].frameLerp;
          animations[i].initialLerp = animations[TORSO_GESTURE].initialLerp;
          animations[i].loopFrames = animations[TORSO_GESTURE].loopFrames;
          animations[i].numFrames = animations[TORSO_GESTURE].numFrames;
          animations[i].reversed = qfalse;
          animations[i].flipflop = qfalse;
          continue;
        }

        break;
      }

      animations[i].firstFrame = atoi(token);

      // leg only frames are adjusted to not count the upper body only frames
      if (i == LEGS_WALKCR)
        skip = animations[LEGS_WALKCR].firstFrame - animations[TORSO_GESTURE].firstFrame;

      if (i >= LEGS_WALKCR && i < TORSO_GETFLAG)
        animations[i].firstFrame -= skip;

      token = COM_Parse(&text_p);
      if (!*token)
        break;

      animations[i].numFrames = atoi(token);
      animations[i].reversed = qfalse;
      animations[i].flipflop = qfalse;

      // if numFrames is negative the animation is reversed
      if (animations[i].numFrames < 0)
      {
        animations[i].numFrames = -animations[i].numFrames;
        animations[i].reversed = qtrue;
      }

      token = COM_Parse(&text_p);

      if (!*token)
        break;

      animations[i].loopFrames = atoi(token);

      token = COM_Parse(&text_p);

      if (!*token)
        break;

      fps = atof(token);
      if (fps == 0)
        fps = 1;

      animations[i].frameLerp = 1000 / fps;
      animations[i].initialLerp = 1000 / fps;
    }

    if (i != MAX_PLAYER_ANIMATIONS)
    {
      CG_Printf("Error parsing animation file: %s", filename);
      return qfalse;
    }
    // crouch backward animation
    memcpy(&animations[LEGS_BACKCR], &animations[LEGS_WALKCR], sizeof(animation_t));
    animations[LEGS_BACKCR].reversed = qtrue;
    // walk backward animation
    memcpy(&animations[LEGS_BACKWALK], &animations[LEGS_WALK], sizeof(animation_t));
    animations[LEGS_BACKWALK].reversed = qtrue;
    // flag moving fast
    animations[FLAG_RUN].firstFrame = 0;
    animations[FLAG_RUN].numFrames = 16;
    animations[FLAG_RUN].loopFrames = 16;
    animations[FLAG_RUN].frameLerp = 1000 / 15;
    animations[FLAG_RUN].initialLerp = 1000 / 15;
    animations[FLAG_RUN].reversed = qfalse;
    // flag not moving or moving slowly
    animations[FLAG_STAND].firstFrame = 16;
    animations[FLAG_STAND].numFrames = 5;
    animations[FLAG_STAND].loopFrames = 0;
    animations[FLAG_STAND].frameLerp = 1000 / 20;
    animations[FLAG_STAND].initialLerp = 1000 / 20;
    animations[FLAG_STAND].reversed = qfalse;
    // flag speeding up
    animations[FLAG_STAND2RUN].firstFrame = 16;
    animations[FLAG_STAND2RUN].numFrames = 5;
    animations[FLAG_STAND2RUN].loopFrames = 1;
    animations[FLAG_STAND2RUN].frameLerp = 1000 / 15;
    animations[FLAG_STAND2RUN].initialLerp = 1000 / 15;
    animations[FLAG_STAND2RUN].reversed = qtrue;
  }
  else
  {
    // read information for each frame
    for(i = 0;i < MAX_NONSEG_PLAYER_ANIMATIONS;i++)
    {
      token = COM_Parse(&text_p);

      if (!*token)
        break;

      animations[i].firstFrame = atoi(token);

      token = COM_Parse(&text_p);
      if (!*token)
        break;

      animations[i].numFrames = atoi(token);
      animations[i].reversed = qfalse;
      animations[i].flipflop = qfalse;

      // if numFrames is negative the animation is reversed
      if (animations[i].numFrames < 0)
      {
        animations[i].numFrames = -animations[i].numFrames;
        animations[i].reversed = qtrue;
      }

      token = COM_Parse(&text_p);

      if (!*token)
        break;

      animations[i].loopFrames = atoi(token);

      token = COM_Parse(&text_p);

      if (!*token)
        break;

      fps = atof(token);
      if (fps == 0)
        fps = 1;

      animations[i].frameLerp = 1000 / fps;
      animations[i].initialLerp = 1000 / fps;
    }

    if (i != MAX_NONSEG_PLAYER_ANIMATIONS)
    {
      CG_Printf("Error parsing animation file: %s", filename);
      return qfalse;
    }

    // walk backward animation
    memcpy(&animations[NSPA_WALKBACK], &animations[NSPA_WALK], sizeof(animation_t));
    animations[NSPA_WALKBACK].reversed = qtrue;
  }

  return qtrue;
}

/*
 ==========================
 CG_RegisterClientSkin
 ==========================
 */
static qboolean
CG_RegisterClientSkin(clientInfo_t *ci, const char *modelName, const char *skinName)
{
  char filename[MAX_QPATH];

  if (!ci->nonsegmented)
  {
    Com_sprintf(filename, sizeof(filename), "models/players/%s/lower_%s.skin", modelName, skinName);
    ci->legsSkin = trap_R_RegisterSkin(filename);
    //zombie
    Com_sprintf(filename, sizeof(filename), "models/players/%s/lower_%s.skin", "human_base", "zombie");
    ci->zombielegsSkin = trap_R_RegisterSkin(filename);
    //Endzombie
    if (!ci->legsSkin)
      Com_Printf("Leg skin load failure: %s\n", filename);

    Com_sprintf(filename, sizeof(filename), "models/players/%s/upper_%s.skin", modelName, skinName);
    ci->torsoSkin = trap_R_RegisterSkin(filename);
    //Zombie
    Com_sprintf(filename, sizeof(filename), "models/players/%s/upper_%s.skin", "human_base", "zombie");
    ci->zombietorsoSkin = trap_R_RegisterSkin(filename);
    //EndZombie
    if (!ci->torsoSkin)
      Com_Printf("Torso skin load failure: %s\n", filename);

    Com_sprintf(filename, sizeof(filename), "models/players/%s/head_%s.skin", modelName, skinName);
    ci->headSkin = trap_R_RegisterSkin(filename);
    //Zombie
    Com_sprintf(filename, sizeof(filename), "models/players/%s/head_%s.skin", "human_base", "zombie");
    ci->zombieheadSkin = trap_R_RegisterSkin(filename);
    //endZombie
    if (!ci->headSkin)
      Com_Printf("Head skin load failure: %s\n", filename);

    if (!ci->legsSkin || !ci->torsoSkin || !ci->headSkin)
      return qfalse;
  }
  else
  {
    Com_sprintf(filename, sizeof(filename), "models/players/%s/nonseg_%s.skin", modelName, skinName);
    ci->nonSegSkin = trap_R_RegisterSkin(filename);
    if (!ci->nonSegSkin)
      Com_Printf("Non-segmented skin load failure: %s\n", filename);

    if (!ci->nonSegSkin)
      return qfalse;
  }

  return qtrue;
}

/*
 ==========================
 CG_RegisterClientModelname
 ==========================
 */
static qboolean
CG_RegisterClientModelname(clientInfo_t *ci, const char *modelName, const char *skinName)
{
  char filename[MAX_QPATH * 2];

  //TA: do this first so the nonsegmented property is set
  // load the animations
  Com_sprintf(filename, sizeof(filename), "models/players/%s/animation.cfg", modelName);
  if (!CG_ParseAnimationFile(filename, ci))
  {
    Com_Printf("Failed to load animation file %s\n", filename);
    return qfalse;
  }

  // load cmodels before models so filecache works

  if (!ci->nonsegmented)
  {
    Com_sprintf(filename, sizeof(filename), "models/players/%s/lower.md3", modelName);
    ci->legsModel = trap_R_RegisterModel(filename);
    if (!ci->legsModel)
    {
      Com_Printf("Failed to load model file %s\n", filename);
      return qfalse;
    }

    Com_sprintf(filename, sizeof(filename), "models/players/%s/upper.md3", modelName);
    ci->torsoModel = trap_R_RegisterModel(filename);
    if (!ci->torsoModel)
    {
      Com_Printf("Failed to load model file %s\n", filename);
      return qfalse;
    }

    Com_sprintf(filename, sizeof(filename), "models/players/%s/head.md3", modelName);
    ci->headModel = trap_R_RegisterModel(filename);
    if (!ci->headModel)
    {
      Com_Printf("Failed to load model file %s\n", filename);
      return qfalse;
    }
  }
  else
  {
    Com_sprintf(filename, sizeof(filename), "models/players/%s/nonseg.md3", modelName);
    ci->nonSegModel = trap_R_RegisterModel(filename);
    if (!ci->nonSegModel)
    {
      Com_Printf("Failed to load model file %s\n", filename);
      return qfalse;
    }
  }

  // if any skins failed to load, return failure
  if (!CG_RegisterClientSkin(ci, modelName, skinName))
  {
    Com_Printf("Failed to load skin file: %s : %s\n", modelName, skinName);
    return qfalse;
  }

  //FIXME: skins do not load without icon present. do we want icons anyway?
  /*  Com_sprintf( filename, sizeof( filename ), "models/players/%s/icon_%s.tga", modelName, skinName );
   ci->modelIcon = trap_R_RegisterShaderNoMip( filename );
   if( !ci->modelIcon )
   {
   Com_Printf( "Failed to load icon file: %s\n", filename );
   return qfalse;
   }*/

  return qtrue;
}

/*
 ====================
 CG_ColorFromString
 ====================
 */
static void
CG_ColorFromString(const char *v, vec3_t color)
{
  int val;

  VectorClear( color );

  val = atoi(v);

  if (val < 1 || val > 7)
  {
    VectorSet( color, 1, 1, 1 );
    return;
  }

  if (val & 1)
    color[2] = 1.0f;

  if (val & 2)
    color[1] = 1.0f;

  if (val & 4)
    color[0] = 1.0f;
}

/*
 ===================
 CG_LoadClientInfo

 Load it now, taking the disk hits
 ===================
 */
static void
CG_LoadClientInfo(clientInfo_t *ci)
{
  const char *dir, *fallback;
  int i;
  const char *s;
  int clientNum;

  if (!CG_RegisterClientModelname(ci, ci->modelName, ci->skinName))
  {
    if (cg_buildScript.integer)
      CG_Error("CG_RegisterClientModelname( %s, %s ) failed", ci->modelName, ci->skinName);

    // fall back
    if (!CG_RegisterClientModelname(ci, DEFAULT_MODEL, "default"))
      CG_Error("DEFAULT_MODEL (%s) failed to register", DEFAULT_MODEL);
  }

  // sounds
  dir = ci->modelName;
  fallback = DEFAULT_MODEL;

  for(i = 0;i < MAX_CUSTOM_SOUNDS;i++)
  {
    s = cg_customSoundNames[i];
    
    //FIX ME: REMOVE THIS AND FIX SOUNDS WHEN  ADDING MORE MODELS
    if (Q_stricmp(dir, "human_base") != 0)
      continue;

    if (!s)
      break;

    // fanny about a bit with sounds that are missing
    if (!CG_FileExists(va("sound/player/%s/%s", dir, s + 1)))
    {
      //file doesn't exist

      if (i == 11 || i == 8) //fall or falling
      {
        ci->sounds[i] = trap_S_RegisterSound("sound/null.wav", qfalse);
      }
      else
      {
        if (i == 9) //gasp
          s = cg_customSoundNames[7]; //pain100_1
        else if (i == 10) //drown
          s = cg_customSoundNames[0]; //death1

        ci->sounds[i] = trap_S_RegisterSound(va("sound/player/%s/%s", dir, s + 1), qfalse);
        if (!ci->sounds[i])
          ci->sounds[i] = trap_S_RegisterSound(va("sound/player/%s/%s", fallback, s + 1), qfalse);
      }
    }
    else
    {
      ci->sounds[i] = trap_S_RegisterSound(va("sound/player/%s/%s", dir, s + 1), qfalse);
      if (!ci->sounds[i])
        ci->sounds[i] = trap_S_RegisterSound(va("sound/player/%s/%s", fallback, s + 1), qfalse);
    }
  }

  if (ci->footsteps == FOOTSTEP_CUSTOM)
  {
    for(i = 0;i < 4;i++)
    {
      ci->customFootsteps[i] = trap_S_RegisterSound(va("sound/player/%s/step%d.wav", dir, i + 1), qfalse);
      if (!ci->customFootsteps[i])
        ci->customFootsteps[i] = trap_S_RegisterSound(va("sound/player/footsteps/step%d.wav", i + 1), qfalse);

      ci->customMetalFootsteps[i] = trap_S_RegisterSound(va("sound/player/%s/clank%d.wav", dir, i + 1), qfalse);
      if (!ci->customMetalFootsteps[i])
        ci->customMetalFootsteps[i] = trap_S_RegisterSound(va("sound/player/footsteps/clank%d.wav", i + 1), qfalse);
    }
  }

  // reset any existing players and bodies, because they might be in bad
  // frames for this new model
  clientNum = ci - cgs.clientinfo;
  for(i = 0;i < MAX_GENTITIES;i++)
  {
    if (cg_entities[i].currentState.clientNum == clientNum && cg_entities[i].currentState.eType == ET_PLAYER)
      CG_ResetPlayerEntity(&cg_entities[i]);
  }
}

/*
 ======================
 CG_CopyClientInfoModel
 ======================
 */
static void
CG_CopyClientInfoModel(clientInfo_t *from, clientInfo_t *to)
{
  VectorCopy( from->headOffset, to->headOffset );
  to->footsteps = from->footsteps;
  to->gender = from->gender;

  to->legsModel = from->legsModel;
  to->legsSkin = from->legsSkin;
  to->zombielegsSkin = from->zombielegsSkin;
  to->torsoModel = from->torsoModel;
  to->torsoSkin = from->torsoSkin;
  to->zombietorsoSkin = from->zombietorsoSkin;
  to->headModel = from->headModel;
  to->headSkin = from->headSkin;
  to->zombieheadSkin = from->zombieheadSkin;
  to->nonSegModel = from->nonSegModel;
  to->nonSegSkin = from->nonSegSkin;
  to->nonsegmented = from->nonsegmented;
  to->modelIcon = from->modelIcon;

  memcpy(to->animations, from->animations, sizeof(to->animations));
  memcpy(to->sounds, from->sounds, sizeof(to->sounds));
  memcpy(to->customFootsteps, from->customFootsteps, sizeof(to->customFootsteps));
  memcpy(to->customMetalFootsteps, from->customMetalFootsteps, sizeof(to->customMetalFootsteps));
}

/*
 ======================
 CG_GetCorpseNum
 ======================
 */
static int
CG_GetCorpseNum(pClass_t class)
{
  int i;
  clientInfo_t *match;
  char *modelName;
  char *skinName;

  modelName = BG_FindModelNameForClass(class);
  skinName = BG_FindSkinNameForClass(class);

  for(i = PCL_NONE + 1;i < PCL_NUM_CLASSES;i++)
  {
    match = &cgs.corpseinfo[i];

    if (!match->infoValid)
      continue;

    if (!Q_stricmp(modelName, match->modelName) && !Q_stricmp(skinName, match->skinName))
    {
      // this clientinfo is identical, so use it's handles
      return i;
    }
  }

  //something has gone horribly wrong
  return -1;
}

/*
 ======================
 CG_ScanForExistingClientInfo
 ======================
 */
static qboolean
CG_ScanForExistingClientInfo(clientInfo_t *ci)
{
  int i;
  clientInfo_t *match;

  for(i = PCL_NONE + 1;i < PCL_NUM_CLASSES;i++)
  {
    match = &cgs.corpseinfo[i];

    if (!match->infoValid)
      continue;

    if (!Q_stricmp(ci->modelName, match->modelName) && !Q_stricmp(ci->skinName, match->skinName))
    {
      // this clientinfo is identical, so use it's handles
      CG_CopyClientInfoModel(match, ci);

      return qtrue;
    }
  }

  //TA: shouldn't happen
  return qfalse;
}

/*
 ======================
 CG_PrecacheClientInfo
 ======================
 */
void
CG_PrecacheClientInfo(pClass_t class, char *model, char *skin)
{
  clientInfo_t *ci;
  clientInfo_t newInfo;

  ci = &cgs.corpseinfo[class];

  // the old value
  memset(&newInfo, 0, sizeof(newInfo));

  // model
  Q_strncpyz(newInfo.modelName, model, sizeof(newInfo.modelName));
  Q_strncpyz(newInfo.headModelName, model, sizeof(newInfo.headModelName));

  // modelName didn not include a skin name
  if (!skin)
  {
    Q_strncpyz(newInfo.skinName, "default", sizeof(newInfo.skinName));
    Q_strncpyz(newInfo.headSkinName, "default", sizeof(newInfo.headSkinName));
  }
  else
  {
    Q_strncpyz(newInfo.skinName, skin, sizeof(newInfo.skinName));
    Q_strncpyz(newInfo.headSkinName, skin, sizeof(newInfo.headSkinName));
  }

  newInfo.infoValid = qtrue;

  //TA: actually register the models
  *ci = newInfo;
  CG_LoadClientInfo(ci);
}

/*
 ======================
 CG_NewClientInfo
 ======================
 */
void
CG_NewClientInfo(int clientNum)
{
  clientInfo_t *ci;
  clientInfo_t newInfo;
  const char *configstring;
  const char *v;
  char *slash;

  ci = &cgs.clientinfo[clientNum];

  configstring = CG_ConfigString(clientNum + CS_PLAYERS);
  if (!configstring[0])
  {
    memset(ci, 0, sizeof(*ci));
    return; // player just left
  }

  // the old value
  memset(&newInfo, 0, sizeof(newInfo));

  // isolate the player's name
  v = Info_ValueForKey(configstring, "n");
  Q_strncpyz(newInfo.name, v, sizeof(newInfo.name));

  // colors
  v = Info_ValueForKey(configstring, "c1");
  CG_ColorFromString(v, newInfo.color1);

  v = Info_ValueForKey(configstring, "c2");
  CG_ColorFromString(v, newInfo.color2);

  // bot skill
  v = Info_ValueForKey(configstring, "skill");
  newInfo.botSkill = atoi(v);

  // handicap
  v = Info_ValueForKey(configstring, "hc");
  newInfo.handicap = atoi(v);

  // wins
  v = Info_ValueForKey(configstring, "w");
  newInfo.wins = atoi(v);

  // losses
  v = Info_ValueForKey(configstring, "l");
  newInfo.losses = atoi(v);

  // team
  v = Info_ValueForKey(configstring, "t");
  newInfo.team = atoi(v);

  // team task
  v = Info_ValueForKey(configstring, "tt");
  newInfo.teamTask = atoi(v);

  // team leader
  v = Info_ValueForKey(configstring, "tl");
  newInfo.teamLeader = atoi(v);

  v = Info_ValueForKey(configstring, "g_redteam");
  Q_strncpyz(newInfo.redTeam, v, MAX_TEAMNAME);

  v = Info_ValueForKey(configstring, "g_blueteam");
  Q_strncpyz(newInfo.blueTeam, v, MAX_TEAMNAME);

  // model
  v = Info_ValueForKey(configstring, "model");
  Q_strncpyz(newInfo.modelName, v, sizeof(newInfo.modelName));

  slash = strchr(newInfo.modelName, '/');

  if (!slash)
  {
    // modelName didn not include a skin name
    Q_strncpyz(newInfo.skinName, "default", sizeof(newInfo.skinName));
  }
  else
  {
    Q_strncpyz(newInfo.skinName, slash + 1, sizeof(newInfo.skinName));
    // truncate modelName
    *slash = 0;
  }

  //CG_Printf( "NCI: %s\n", v );

  // head model
  v = Info_ValueForKey(configstring, "hmodel");
  Q_strncpyz(newInfo.headModelName, v, sizeof(newInfo.headModelName));

  slash = strchr(newInfo.headModelName, '/');

  if (!slash)
  {
    // modelName didn not include a skin name
    Q_strncpyz(newInfo.headSkinName, "default", sizeof(newInfo.headSkinName));
  }
  else
  {
    Q_strncpyz(newInfo.headSkinName, slash + 1, sizeof(newInfo.headSkinName));
    // truncate modelName
    *slash = 0;
  }

  // replace whatever was there with the new one
  newInfo.infoValid = qtrue;
  *ci = newInfo;

  // scan for an existing clientinfo that matches this modelname
  // so we can avoid loading checks if possible
  if (!CG_ScanForExistingClientInfo(ci))
    CG_LoadClientInfo(ci);
}

/*
 =============================================================================

 PLAYER ANIMATION

 =============================================================================
 */

/*
 ===============
 CG_SetLerpFrameAnimation

 may include ANIM_TOGGLEBIT
 ===============
 */
static void
CG_SetLerpFrameAnimation(clientInfo_t *ci, lerpFrame_t *lf, int newAnimation)
{
  animation_t *anim;

  lf->animationNumber = newAnimation;
  newAnimation &= ~ANIM_TOGGLEBIT;

  if (newAnimation < 0 || newAnimation >= MAX_PLAYER_TOTALANIMATIONS)
    CG_Error("Bad animation number: %i", newAnimation);

  anim = &ci->animations[newAnimation];

  lf->animation = anim;
  lf->animationTime = lf->frameTime + anim->initialLerp;

  if (cg_debugAnim.integer)
    CG_Printf("Anim: %i\n", newAnimation);
}

/*
 ===============
 CG_RunPlayerLerpFrame

 Sets cg.snap, cg.oldFrame, and cg.backlerp
 cg.time should be between oldFrameTime and frameTime after exit
 ===============
 */
static void
CG_RunPlayerLerpFrame(clientInfo_t *ci, lerpFrame_t *lf, int newAnimation, float speedScale)
{
  int f, numFrames;
  animation_t *anim;

  // debugging tool to get no animations
  if (cg_animSpeed.integer == 0)
  {
    lf->oldFrame = lf->frame = lf->backlerp = 0;
    return;
  }

  // see if the animation sequence is switching
  if (newAnimation != lf->animationNumber || !lf->animation)
  {
    CG_SetLerpFrameAnimation(ci, lf, newAnimation);
  }

  // if we have passed the current frame, move it to
  // oldFrame and calculate a new frame
  if (cg.time >= lf->frameTime)
  {
    lf->oldFrame = lf->frame;
    lf->oldFrameTime = lf->frameTime;

    // get the next frame based on the animation
    anim = lf->animation;
    if (!anim->frameLerp)
      return; // shouldn't happen

    if (cg.time < lf->animationTime)
      lf->frameTime = lf->animationTime; // initial lerp
    else
      lf->frameTime = lf->oldFrameTime + anim->frameLerp;

    f = (lf->frameTime - lf->animationTime) / anim->frameLerp;
    f *= speedScale; // adjust for haste, etc
    numFrames = anim->numFrames;

    if (anim->flipflop)
      numFrames *= 2;

    if (f >= numFrames)
    {
      f -= numFrames;
      if (anim->loopFrames)
      {
        f %= anim->loopFrames;
        f += anim->numFrames - anim->loopFrames;
      }
      else
      {
        f = numFrames - 1;
        // the animation is stuck at the end, so it
        // can immediately transition to another sequence
        lf->frameTime = cg.time;
      }
    }

    if (anim->reversed)
      lf->frame = anim->firstFrame + anim->numFrames - 1 - f;
    else if (anim->flipflop && f >= anim->numFrames)
      lf->frame = anim->firstFrame + anim->numFrames - 1 - (f % anim->numFrames);
    else
      lf->frame = anim->firstFrame + f;

    if (cg.time > lf->frameTime)
    {
      lf->frameTime = cg.time;

      if (cg_debugAnim.integer)
        CG_Printf("Clamp lf->frameTime\n");
    }
  }

  if (lf->frameTime > cg.time + 200)
    lf->frameTime = cg.time;

  if (lf->oldFrameTime > cg.time)
    lf->oldFrameTime = cg.time;

  // calculate current lerp value
  if (lf->frameTime == lf->oldFrameTime)
    lf->backlerp = 0;
  else
    lf->backlerp = 1.0 - (float) (cg.time - lf->oldFrameTime) / (lf->frameTime - lf->oldFrameTime);
}

/*
 ===============
 CG_ClearLerpFrame
 ===============
 */
static void
CG_ClearLerpFrame(clientInfo_t *ci, lerpFrame_t *lf, int animationNumber)
{
  lf->frameTime = lf->oldFrameTime = cg.time;
  CG_SetLerpFrameAnimation(ci, lf, animationNumber);
  lf->oldFrame = lf->frame = lf->animation->firstFrame;
}

/*
 ===============
 CG_PlayerAnimation
 ===============
 */
static void
CG_PlayerAnimation(centity_t *cent, int *legsOld, int *legs, float *legsBackLerp, int *torsoOld, int *torso, float *torsoBackLerp)
{
  clientInfo_t *ci;
  int clientNum;
  float speedScale = 1.0f;

  clientNum = cent->currentState.clientNum;

  if (cg_noPlayerAnims.integer)
  {
    *legsOld = *legs = *torsoOld = *torso = 0;
    return;
  }

  ci = &cgs.clientinfo[clientNum];

  // do the shuffle turn frames locally
  if (cent->pe.legs.yawing && (cent->currentState.legsAnim & ~ANIM_TOGGLEBIT) == LEGS_IDLE)
    CG_RunPlayerLerpFrame(ci, &cent->pe.legs, LEGS_TURN, speedScale);
  else
    CG_RunPlayerLerpFrame(ci, &cent->pe.legs, cent->currentState.legsAnim, speedScale);

  *legsOld = cent->pe.legs.oldFrame;
  *legs = cent->pe.legs.frame;
  *legsBackLerp = cent->pe.legs.backlerp;

  CG_RunPlayerLerpFrame(ci, &cent->pe.torso, cent->currentState.torsoAnim, speedScale);

  *torsoOld = cent->pe.torso.oldFrame;
  *torso = cent->pe.torso.frame;
  *torsoBackLerp = cent->pe.torso.backlerp;
}

/*
 ===============
 CG_PlayerNonSegAnimation
 ===============
 */
static void
CG_PlayerNonSegAnimation(centity_t *cent, int *nonSegOld, int *nonSeg, float *nonSegBackLerp)
{
  clientInfo_t *ci;
  int clientNum;
  float speedScale = 1.0f;

  clientNum = cent->currentState.clientNum;

  if (cg_noPlayerAnims.integer)
  {
    *nonSegOld = *nonSeg = 0;
    return;
  }

  ci = &cgs.clientinfo[clientNum];

  // do the shuffle turn frames locally
  if (cent->pe.nonseg.yawing && (cent->currentState.legsAnim & ~ANIM_TOGGLEBIT) == NSPA_STAND)
    CG_RunPlayerLerpFrame(ci, &cent->pe.nonseg, NSPA_TURN, speedScale);
  else
    CG_RunPlayerLerpFrame(ci, &cent->pe.nonseg, cent->currentState.legsAnim, speedScale);

  *nonSegOld = cent->pe.nonseg.oldFrame;
  *nonSeg = cent->pe.nonseg.frame;
  *nonSegBackLerp = cent->pe.nonseg.backlerp;
}

/*
 =============================================================================

 PLAYER ANGLES

 =============================================================================
 */

/*
 ==================
 CG_SwingAngles
 ==================
 */
static void
CG_SwingAngles(float destination, float swingTolerance, float clampTolerance, float speed, float *angle, qboolean *swinging)
{
  float swing;
  float move;
  float scale;

  if (!*swinging)
  {
    // see if a swing should be started
    swing = AngleSubtract(*angle, destination);

    if (swing > swingTolerance || swing < -swingTolerance)
      *swinging = qtrue;
  }

  if (!*swinging)
    return;

  // modify the speed depending on the delta
  // so it doesn't seem so linear
  swing = AngleSubtract(destination, *angle);
  scale = fabs(swing);

  if (scale < swingTolerance * 0.5)
    scale = 0.5;
  else if (scale < swingTolerance)
    scale = 1.0;
  else
    scale = 2.0;

  // swing towards the destination angle
  if (swing >= 0)
  {
    move = cg.frametime * scale * speed;

    if (move >= swing)
    {
      move = swing;
      *swinging = qfalse;
    }
    *angle = AngleMod(*angle + move);
  }
  else if (swing < 0)
  {
    move = cg.frametime * scale * -speed;

    if (move <= swing)
    {
      move = swing;
      *swinging = qfalse;
    }
    *angle = AngleMod(*angle + move);
  }

  // clamp to no more than tolerance
  swing = AngleSubtract(destination, *angle);
  if (swing > clampTolerance)
    *angle = AngleMod(destination - (clampTolerance - 1));
  else if (swing < -clampTolerance)
    *angle = AngleMod(destination + (clampTolerance - 1));
}

/*
 =================
 CG_AddPainTwitch
 =================
 */
static void
CG_AddPainTwitch(centity_t *cent, vec3_t torsoAngles)
{
  int t;
  float f;

  t = cg.time - cent->pe.painTime;

  if (t >= PAIN_TWITCH_TIME)
    return;

  f = 1.0 - (float) t / PAIN_TWITCH_TIME;

  if (cent->pe.painDirection)
    torsoAngles[ROLL] += 20 * f;
  else
    torsoAngles[ROLL] -= 20 * f;
}

/*
 ===============
 CG_PlayerAngles

 Handles seperate torso motion

 legs pivot based on direction of movement

 head always looks exactly at cent->lerpAngles

 if motion < 20 degrees, show in head only
 if < 45 degrees, also show in torso
 ===============
 */
static void
CG_PlayerAngles(centity_t *cent, vec3_t srcAngles, vec3_t legs[3], vec3_t torso[3], vec3_t head[3])
{
  vec3_t legsAngles, torsoAngles, headAngles;
  float dest;
  static int movementOffsets[8] =
  { 0, 22, 45, -22, 0, 22, -45, -22 };
  vec3_t velocity;
  float speed;
  int dir, clientNum;
  clientInfo_t *ci;

  VectorCopy( srcAngles, headAngles );
  headAngles[YAW] = AngleMod(headAngles[YAW]);
  VectorClear( legsAngles );
  VectorClear( torsoAngles );

  // --------- yaw -------------

  // allow yaw to drift a bit
  if ((cent->currentState.legsAnim & ~ANIM_TOGGLEBIT) != LEGS_IDLE || (cent->currentState.torsoAnim & ~ANIM_TOGGLEBIT) != TORSO_STAND)
  {
    // if not standing still, always point all in the same direction
    cent->pe.torso.yawing = qtrue; // always center
    cent->pe.torso.pitching = qtrue; // always center
    cent->pe.legs.yawing = qtrue; // always center
  }

  // adjust legs for movement dir
  if (cent->currentState.eFlags & EF_DEAD)
  {
    // don't let dead bodies twitch
    dir = 0;
  }
  else
  {
    //TA: did use angles2.. now uses time2.. looks a bit funny but time2 isn't used othwise
    dir = cent->currentState.time2;
    if (dir < 0 || dir > 7)
      CG_Error("Bad player movement angle");
  }

  legsAngles[YAW] = headAngles[YAW] + movementOffsets[dir];
  torsoAngles[YAW] = headAngles[YAW] + 0.25 * movementOffsets[dir];

  // torso
  if (cent->currentState.eFlags & EF_DEAD)
  {
    CG_SwingAngles(torsoAngles[YAW], 0, 0, cg_swingSpeed.value, &cent->pe.torso.yawAngle, &cent->pe.torso.yawing);
    CG_SwingAngles(legsAngles[YAW], 0, 0, cg_swingSpeed.value, &cent->pe.legs.yawAngle, &cent->pe.legs.yawing);
  }
  else
  {
    CG_SwingAngles(torsoAngles[YAW], 25, 90, cg_swingSpeed.value, &cent->pe.torso.yawAngle, &cent->pe.torso.yawing);
    CG_SwingAngles(legsAngles[YAW], 40, 90, cg_swingSpeed.value, &cent->pe.legs.yawAngle, &cent->pe.legs.yawing);
  }

  torsoAngles[YAW] = cent->pe.torso.yawAngle;
  legsAngles[YAW] = cent->pe.legs.yawAngle;

  // --------- pitch -------------

  // only show a fraction of the pitch angle in the torso
  if (headAngles[PITCH] > 180)
    dest = (-360 + headAngles[PITCH]) * 0.75f;
  else
    dest = headAngles[PITCH] * 0.75f;

  CG_SwingAngles(dest, 15, 30, 0.1f, &cent->pe.torso.pitchAngle, &cent->pe.torso.pitching);
  torsoAngles[PITCH] = cent->pe.torso.pitchAngle;

  //
  clientNum = cent->currentState.clientNum;

  if (clientNum >= 0 && clientNum < MAX_CLIENTS)
  {
    ci = &cgs.clientinfo[clientNum];
    if (ci->fixedtorso)
      torsoAngles[PITCH] = 0.0f;
  }

  // --------- roll -------------


  // lean towards the direction of travel
  VectorCopy( cent->currentState.pos.trDelta, velocity );
  speed = VectorNormalize(velocity);

  if (speed)
  {
    vec3_t axis[3];
    float side;

    speed *= 0.05f;

    AnglesToAxis(legsAngles, axis);
    side = speed * DotProduct( velocity, axis[ 1 ] );
    legsAngles[ROLL] -= side;

    side = speed * DotProduct( velocity, axis[ 0 ] );
    legsAngles[PITCH] += side;
  }

  //
  clientNum = cent->currentState.clientNum;

  if (clientNum >= 0 && clientNum < MAX_CLIENTS)
  {
    ci = &cgs.clientinfo[clientNum];

    if (ci->fixedlegs)
    {
      legsAngles[YAW] = torsoAngles[YAW];
      legsAngles[PITCH] = 0.0f;
      legsAngles[ROLL] = 0.0f;
    }
  }

  // pain twitch
  CG_AddPainTwitch(cent, torsoAngles);

  // pull the angles back out of the hierarchial chain
  AnglesSubtract(headAngles, torsoAngles, headAngles);
  AnglesSubtract(torsoAngles, legsAngles, torsoAngles);
  AnglesToAxis(legsAngles, legs);
  AnglesToAxis(torsoAngles, torso);
  AnglesToAxis(headAngles, head);
}

#define MODEL_WWSMOOTHTIME 200

/*
 ===============
 CG_PlayerWWSmoothing

 Smooth the angles of transitioning wall walkers
 ===============
 */
static void
CG_PlayerWWSmoothing(centity_t *cent, vec3_t in[3], vec3_t out[3])
{
  entityState_t *es = &cent->currentState;
  int i;
  vec3_t surfNormal, rotAxis, temp;
  vec3_t refNormal =
  { 0.0f, 0.0f, 1.0f };
  vec3_t ceilingNormal =
  { 0.0f, 0.0f, -1.0f };
  float stLocal, sFraction, rotAngle;
  vec3_t inAxis[3], lastAxis[3], outAxis[3];

  //set surfNormal
  if (!(es->eFlags & EF_WALLCLIMB))
    VectorCopy( refNormal, surfNormal );
  else if (!(es->eFlags & EF_WALLCLIMBCEILING))
    VectorCopy( es->angles2, surfNormal );
else    VectorCopy( ceilingNormal, surfNormal );

    AxisCopy( in, inAxis );

    if( !VectorCompare( surfNormal, cent->pe.lastNormal ) )
    {
      //if we moving from the ceiling to the floor special case
      //( x product of colinear vectors is undefined)
      if( VectorCompare( ceilingNormal, cent->pe.lastNormal ) &&
          VectorCompare( refNormal, surfNormal ) )
      {
        VectorCopy( in[ 1 ], rotAxis );
        rotAngle = 180.0f;
      }
      else
      {
        AxisCopy( cent->pe.lastAxis, lastAxis );
        rotAngle = DotProduct( inAxis[ 0 ], lastAxis[ 0 ] ) +
        DotProduct( inAxis[ 1 ], lastAxis[ 1 ] ) +
        DotProduct( inAxis[ 2 ], lastAxis[ 2 ] );

        rotAngle = RAD2DEG( acos( ( rotAngle - 1.0f ) / 2.0f ) );

        CrossProduct( lastAxis[ 0 ], inAxis[ 0 ], temp );
        VectorCopy( temp, rotAxis );
        CrossProduct( lastAxis[ 1 ], inAxis[ 1 ], temp );
        VectorAdd( rotAxis, temp, rotAxis );
        CrossProduct( lastAxis[ 2 ], inAxis[ 2 ], temp );
        VectorAdd( rotAxis, temp, rotAxis );

        VectorNormalize( rotAxis );
      }

      //iterate through smooth array
      for( i = 0; i < MAXSMOOTHS; i++ )
      {
        //found an unused index in the smooth array
        if( cent->pe.sList[ i ].time + MODEL_WWSMOOTHTIME < cg.time )
        {
          //copy to array and stop
          VectorCopy( rotAxis, cent->pe.sList[ i ].rotAxis );
          cent->pe.sList[ i ].rotAngle = rotAngle;
          cent->pe.sList[ i ].time = cg.time;
          break;
        }
      }
    }

    //iterate through ops
    for( i = MAXSMOOTHS - 1; i >= 0; i-- )
    {
      //if this op has time remaining, perform it
      if( cg.time < cent->pe.sList[ i ].time + MODEL_WWSMOOTHTIME )
      {
        stLocal = 1.0f - ( ( ( cent->pe.sList[ i ].time + MODEL_WWSMOOTHTIME ) - cg.time ) / MODEL_WWSMOOTHTIME );
        sFraction = -( cos( stLocal * M_PI ) + 1.0f ) / 2.0f;

        RotatePointAroundVector( outAxis[ 0 ], cent->pe.sList[ i ].rotAxis,
            inAxis[ 0 ], sFraction * cent->pe.sList[ i ].rotAngle );
        RotatePointAroundVector( outAxis[ 1 ], cent->pe.sList[ i ].rotAxis,
            inAxis[ 1 ], sFraction * cent->pe.sList[ i ].rotAngle );
        RotatePointAroundVector( outAxis[ 2 ], cent->pe.sList[ i ].rotAxis,
            inAxis[ 2 ], sFraction * cent->pe.sList[ i ].rotAngle );

        AxisCopy( outAxis, inAxis );
      }
    }

    //outAxis has been copied to inAxis
    AxisCopy( inAxis, out );
  }

  /*
   ===============
   CG_PlayerNonSegAngles

   Resolve angles for non-segmented models
   ===============
   */
static void
CG_PlayerNonSegAngles(centity_t *cent, vec3_t srcAngles, vec3_t nonSegAxis[3])
{
  vec3_t localAngles;
  vec3_t velocity;
  float speed;
  int dir;
  entityState_t *es = &cent->currentState;
  vec3_t surfNormal;
  vec3_t ceilingNormal =
  { 0.0f, 0.0f, -1.0f };

  VectorCopy( srcAngles, localAngles );
  localAngles[YAW] = AngleMod(localAngles[YAW]);
  localAngles[PITCH] = 0.0f;
  localAngles[ROLL] = 0.0f;

  //set surfNormal
  if (!(es->eFlags & EF_WALLCLIMBCEILING))
    VectorCopy( es->angles2, surfNormal );
else    VectorCopy( ceilingNormal, surfNormal );

    //make sure that WW transitions don't cause the swing stuff to go nuts
    if( !VectorCompare( surfNormal, cent->pe.lastNormal ) )
    {
      //stop CG_SwingAngles having an eppy
      cent->pe.nonseg.yawAngle = localAngles[ YAW ];
      cent->pe.nonseg.yawing = qfalse;
    }

    // --------- yaw -------------

    // allow yaw to drift a bit
    if( ( cent->currentState.legsAnim & ~ANIM_TOGGLEBIT ) != NSPA_STAND )
    {
      // if not standing still, always point all in the same direction
      cent->pe.nonseg.yawing = qtrue; // always center
    }

    // adjust legs for movement dir
    if( cent->currentState.eFlags & EF_DEAD )
    {
      // don't let dead bodies twitch
      dir = 0;
    }
    else
    {
      //TA: did use angles2.. now uses time2.. looks a bit funny but time2 isn't used othwise
      dir = cent->currentState.time2;
      if( dir < 0 || dir > 7 )
      CG_Error( "Bad player movement angle" );
    }

    // torso
    if( cent->currentState.eFlags & EF_DEAD )
    {
      CG_SwingAngles( localAngles[ YAW ], 0, 0, cg_swingSpeed.value,
          &cent->pe.nonseg.yawAngle, &cent->pe.nonseg.yawing );
    }
    else
    {
      CG_SwingAngles( localAngles[ YAW ], 40, 90, cg_swingSpeed.value,
          &cent->pe.nonseg.yawAngle, &cent->pe.nonseg.yawing );
    }

    localAngles[ YAW ] = cent->pe.nonseg.yawAngle;

    // --------- pitch -------------

    //NO PITCH!


    // --------- roll -------------


    // lean towards the direction of travel
    VectorCopy( cent->currentState.pos.trDelta, velocity );
    speed = VectorNormalize( velocity );

    if( speed )
    {
      vec3_t axis[ 3 ];
      float side;

      //much less than with the regular model system
      speed *= 0.01f;

      AnglesToAxis( localAngles, axis );
      side = speed * DotProduct( velocity, axis[ 1 ] );
      localAngles[ ROLL ] -= side;

      side = speed * DotProduct( velocity, axis[ 0 ] );
      localAngles[ PITCH ] += side;
    }

    //FIXME: PAIN[123] animations?
    // pain twitch
    //CG_AddPainTwitch( cent, torsoAngles );

    AnglesToAxis( localAngles, nonSegAxis );
  }

  //==========================================================================

  /*
   ===============
   CG_PlayerUpgrade
   ===============
   */
static void
CG_PlayerUpgrades(centity_t *cent, refEntity_t *torso)
{
  int held, active;
  refEntity_t jetpack;
  refEntity_t battpack;
  refEntity_t flash;
  entityState_t *es = &cent->currentState;

  held = es->modelindex;
  active = es->modelindex2;

  /*if( held & ( 1 << UP_JETPACK ) )
   {
   memset( &jetpack, 0, sizeof( jetpack ) );
   VectorCopy( torso->lightingOrigin, jetpack.lightingOrigin );
   jetpack.shadowPlane = torso->shadowPlane;
   jetpack.renderfx = torso->renderfx;

   jetpack.hModel = cgs.media.jetpackModel;

   //identity matrix
   AxisCopy( axisDefault, jetpack.axis );

   //FIXME: change to tag_back when it exists
   CG_PositionRotatedEntityOnTag( &jetpack, torso, torso->hModel, "tag_head" );

   trap_R_AddRefEntityToScene( &jetpack );

   if( active & ( 1 << UP_JETPACK ) )
   {
   if( es->pos.trDelta[ 2 ] > 10.0f )
   {
   if( cent->jetPackState != JPS_ASCENDING )
   {
   if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
   CG_DestroyParticleSystem( &cent->jetPackPS );

   cent->jetPackPS = CG_SpawnNewParticleSystem( cgs.media.jetPackAscendPS );
   cent->jetPackState = JPS_ASCENDING;
   }

   trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
   vec3_origin, cgs.media.jetpackAscendSound );
   }
   else if( es->pos.trDelta[ 2 ] < -10.0f )
   {
   if( cent->jetPackState != JPS_DESCENDING )
   {
   if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
   CG_DestroyParticleSystem( &cent->jetPackPS );

   cent->jetPackPS = CG_SpawnNewParticleSystem( cgs.media.jetPackDescendPS );
   cent->jetPackState = JPS_DESCENDING;
   }

   trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
   vec3_origin, cgs.media.jetpackDescendSound );
   }
   else
   {
   if( cent->jetPackState != JPS_HOVERING )
   {
   if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
   CG_DestroyParticleSystem( &cent->jetPackPS );

   cent->jetPackPS = CG_SpawnNewParticleSystem( cgs.media.jetPackHoverPS );
   cent->jetPackState = JPS_HOVERING;
   }

   trap_S_AddLoopingSound( cent->currentState.number, cent->lerpOrigin,
   vec3_origin, cgs.media.jetpackIdleSound );
   }

   memset( &flash, 0, sizeof( flash ) );
   VectorCopy( torso->lightingOrigin, flash.lightingOrigin );
   flash.shadowPlane = torso->shadowPlane;
   flash.renderfx = torso->renderfx;

   flash.hModel = cgs.media.jetpackFlashModel;
   if( !flash.hModel )
   return;

   AxisCopy( axisDefault, flash.axis );

   CG_PositionRotatedEntityOnTag( &flash, &jetpack, jetpack.hModel, "tag_flash" );
   trap_R_AddRefEntityToScene( &flash );

   if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
   {
   CG_SetAttachmentTag( &cent->jetPackPS->attachment,
   jetpack, jetpack.hModel, "tag_flash" );
   CG_SetAttachmentCent( &cent->jetPackPS->attachment, cent );
   CG_AttachToTag( &cent->jetPackPS->attachment );
   }
   }
   else if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
   {
   CG_DestroyParticleSystem( &cent->jetPackPS );
   cent->jetPackState = JPS_OFF;
   }
   }
   else*/
  if (CG_IsParticleSystemValid(&cent->jetPackPS))
  {
    CG_DestroyParticleSystem(&cent->jetPackPS);
    cent->jetPackState = JPS_OFF;
  }

  if (held & (1 << UP_BATTPACK))
  {
    memset(&battpack, 0, sizeof(battpack));
    VectorCopy( torso->lightingOrigin, battpack.lightingOrigin );
    battpack.shadowPlane = torso->shadowPlane;
    battpack.renderfx = torso->renderfx;

    battpack.hModel = cgs.media.battpackModel;

    //identity matrix
    AxisCopy(axisDefault, battpack.axis);

    //FIXME: change to tag_back when it exists
    CG_PositionRotatedEntityOnTag(&battpack, torso, torso->hModel, "tag_head");

    trap_R_AddRefEntityToScene(&battpack);
  }

  if (es->eFlags & EF_BLOBLOCKED)
  {
    vec3_t temp, origin, up =
    { 0.0f, 0.0f, 1.0f };
    trace_t tr;
    float size;

    VectorCopy( es->pos.trBase, temp );
    temp[2] -= 4096.0f;

    CG_Trace(&tr, es->pos.trBase, NULL, NULL, temp, es->number, MASK_SOLID);
    VectorCopy( tr.endpos, origin );

    size = 32.0f;

    if (cgs.clientinfo[cent->currentState.number].team == PTE_ALIENS)
    {
      if (size > 0.0f)
        CG_ImpactMark(cgs.media.aliencreepShader, origin, up, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, qfalse, size, qtrue);
    }
    if (cgs.clientinfo[cent->currentState.number].team == PTE_HUMANS)
    {
      CG_ImpactMark(cgs.media.humancreepShader, origin, up, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, qfalse, size, qtrue);
    }
  }
}

/*
 ===============
 CG_PlayerFloatSprite

 Float a sprite over the player's head
 ===============
 */
static void
CG_PlayerFloatSprite(centity_t *cent, qhandle_t shader, vec4_t color)
{
  int rf;
  refEntity_t ent;

  if (cent->currentState.number == cg.snap->ps.clientNum && !cg.renderingThirdPerson)
    rf = RF_THIRD_PERSON; // only show in mirrors
  else
    rf = 0;

  memset(&ent, 0, sizeof(ent));
  VectorCopy( cent->lerpOrigin, ent.origin );

  ent.reType = RT_SPRITE;
  ent.customShader = shader;
  ent.radius = 10;
  ent.renderfx = rf;
  ent.shaderRGBA[0] = color[0] * 255;
  ent.shaderRGBA[1] = color[1] * 255;
  ent.shaderRGBA[2] = color[2] * 255;
  ent.shaderRGBA[3] = color[3] * 255;

  // Find the proper height to float the sprite
  ent.origin[2] += BG_FindSpriteHeightForClass((cent->currentState.powerups >> 8) & 0xFF);

  // Bsuit offset hack
  // TODO: chat balloons for suits are inside their heads


  trap_R_AddRefEntityToScene(&ent);
}

/*
 ===============
 CG_PlayerSprites

 Float sprites over the player's head
 ===============
 */
static void
CG_PlayerSprites(centity_t *cent)
{
  if (cent->currentState.eFlags & EF_CONNECTION)
  {
    vec4_t white =
    { 1.f, 1.f, 1.f, 1.f };
    CG_PlayerFloatSprite(cent, cgs.media.connectionShader, white);
    return;
  }

  if (cent->currentState.eFlags & EF_TALK)
  {
    //TA: the masses have decreed this to be wrong

    // ... because you coded it wrong. --Risujin
    vec4_t color_human =
    { 0.04f, 0.71f, 0.88f, 1.0f };
    vec4_t color_alien =
    { 0.75f, 0.00f, 0.00f, 1.0f };
    if (cgs.clientinfo[cent->currentState.number].team == PTE_ALIENS)
      CG_PlayerFloatSprite(cent, cgs.media.balloonShader, color_alien);
    else if (cgs.clientinfo[cent->currentState.number].team == PTE_HUMANS)
      CG_PlayerFloatSprite(cent, cgs.media.balloonShader, color_human);

  }
}

/*
 ===============
 CG_PlayerShadow

 Returns the Z component of the surface being shadowed

 should it return a full plane instead of a Z?
 ===============
 */
#define SHADOW_DISTANCE   128
static qboolean
CG_PlayerShadow(centity_t *cent, float *shadowPlane, pClass_t class)
{
  vec3_t end, mins, maxs;
  trace_t trace;
  float alpha;
  entityState_t *es = &cent->currentState;
  vec3_t surfNormal =
  { 0.0f, 0.0f, 1.0f };

  BG_FindBBoxForClass(class, mins, maxs, NULL, NULL, NULL);
  mins[2] = 0.0f;
  maxs[2] = 2.0f;

  if (es->eFlags & EF_WALLCLIMB)
  {
    if (es->eFlags & EF_WALLCLIMBCEILING)
      VectorSet( surfNormal, 0.0f, 0.0f, -1.0f );
else      VectorCopy( es->angles2, surfNormal );
    }

    *shadowPlane = 0;

    if( cg_shadows.integer == 0 )
    return qfalse;

    // send a trace down from the player to the ground
    VectorCopy( cent->lerpOrigin, end );
    VectorMA( cent->lerpOrigin, -SHADOW_DISTANCE, surfNormal, end );

    trap_CM_BoxTrace( &trace, cent->lerpOrigin, end, mins, maxs, 0, MASK_PLAYERSOLID );

    // no shadow if too high
    if( trace.fraction == 1.0 || trace.startsolid || trace.allsolid )
    return qfalse;

    //TA: FIXME: stencil shadows will be broken for walls.
    //           Unfortunately there isn't much that can be
    //           done since Q3 references only the Z coord
    //           of the shadowPlane
    if( surfNormal[ 2 ] < 0.0f )
    *shadowPlane = trace.endpos[ 2 ] - 1.0f;
    else
    *shadowPlane = trace.endpos[ 2 ] + 1.0f;

    if( cg_shadows.integer != 1 ) // no mark for stencil or projection shadows
    return qtrue;

    // fade the shadow out with height
    alpha = 1.0 - trace.fraction;

    // add the mark as a temporary, so it goes directly to the renderer
    // without taking a spot in the cg_marks array
    CG_ImpactMark( cgs.media.shadowMarkShader, trace.endpos, trace.plane.normal,
        cent->pe.legs.yawAngle, 0.0f, 0.0f, 0.0f, alpha, qfalse,
        24.0f * BG_FindShadowScaleForClass( class ), qtrue );

    return qtrue;
  }

  /*
   ===============
   CG_PlayerSplash

   Draw a mark at the water surface
   ===============
   */
static void
CG_PlayerSplash(centity_t *cent, pClass_t class)
{
  vec3_t start, end;
  vec3_t mins, maxs;
  trace_t trace;
  int contents;

  if (!cg_shadows.integer)
    return;

  BG_FindBBoxForClass(class, mins, maxs, NULL, NULL, NULL);

  VectorCopy( cent->lerpOrigin, end );
  end[2] += mins[2];

  // if the feet aren't in liquid, don't make a mark
  // this won't handle moving water brushes, but they wouldn't draw right anyway...
  contents = trap_CM_PointContents(end, 0);

  if (!(contents & (CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA)))
    return;

  VectorCopy( cent->lerpOrigin, start );
  start[2] += 32;

  // if the head isn't out of liquid, don't make a mark
  contents = trap_CM_PointContents(start, 0);

  if (contents & (CONTENTS_SOLID | CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA))
    return;

  // trace down to find the surface
  trap_CM_BoxTrace(&trace, start, end, NULL, NULL, 0, (CONTENTS_WATER | CONTENTS_SLIME | CONTENTS_LAVA));

  if (trace.fraction == 1.0f)
    return;

  CG_ImpactMark(cgs.media.wakeMarkShader, trace.endpos, trace.plane.normal, cent->pe.legs.yawAngle, 1.0f, 1.0f, 1.0f, 1.0f, qfalse, 32.0f
      * BG_FindShadowScaleForClass(class), qtrue);
}

/*
 =================
 CG_LightVerts
 =================
 */
int
CG_LightVerts(vec3_t normal, int numVerts, polyVert_t *verts)
{
  int i, j;
  float incoming;
  vec3_t ambientLight;
  vec3_t lightDir;
  vec3_t directedLight;

  trap_R_LightForPoint(verts[0].xyz, ambientLight, directedLight, lightDir);

  for(i = 0;i < numVerts;i++)
  {
    incoming = DotProduct( normal, lightDir );

    if (incoming <= 0)
    {
      verts[i].modulate[0] = ambientLight[0];
      verts[i].modulate[1] = ambientLight[1];
      verts[i].modulate[2] = ambientLight[2];
      verts[i].modulate[3] = 255;
      continue;
    }

    j = (ambientLight[0] + incoming * directedLight[0]);

    if (j > 255)
      j = 255;

    verts[i].modulate[0] = j;

    j = (ambientLight[1] + incoming * directedLight[1]);

    if (j > 255)
      j = 255;

    verts[i].modulate[1] = j;

    j = (ambientLight[2] + incoming * directedLight[2]);

    if (j > 255)
      j = 255;

    verts[i].modulate[2] = j;

    verts[i].modulate[3] = 255;
  }
  return qtrue;
}

/*
 =================
 CG_LightFromDirection
 =================
 */
int
CG_LightFromDirection(vec3_t point, vec3_t direction)
{
  int j;
  float incoming;
  vec3_t ambientLight;
  vec3_t lightDir;
  vec3_t directedLight;
  vec3_t result;

  trap_R_LightForPoint(point, ambientLight, directedLight, lightDir);

  incoming = DotProduct( direction, lightDir );

  if (incoming <= 0)
  {
    result[0] = ambientLight[0];
    result[1] = ambientLight[1];
    result[2] = ambientLight[2];
    return (int) ((float) (result[0] + result[1] + result[2]) / 3.0f);
  }

  j = (ambientLight[0] + incoming * directedLight[0]);

  if (j > 255)
    j = 255;

  result[0] = j;

  j = (ambientLight[1] + incoming * directedLight[1]);

  if (j > 255)
    j = 255;

  result[1] = j;

  j = (ambientLight[2] + incoming * directedLight[2]);

  if (j > 255)
    j = 255;

  result[2] = j;

  return (int) ((float) (result[0] + result[1] + result[2]) / 3.0f);
}

/*
 =================
 CG_AmbientLight
 =================
 */
int
CG_AmbientLight(vec3_t point)
{
  vec3_t ambientLight;
  vec3_t lightDir;
  vec3_t directedLight;
  vec3_t result;

  trap_R_LightForPoint(point, ambientLight, directedLight, lightDir);

  result[0] = ambientLight[0];
  result[1] = ambientLight[1];
  result[2] = ambientLight[2];
  return (int) ((float) (result[0] + result[1] + result[2]) / 3.0f);
}

#define TRACE_DEPTH 32.0f

#define HEALTH_BAR_WIDTH  10.0f
#define HEALTH_BAR_HEIGHT 2.0f
/*
 ==================
 CG_BuildableHealthBar
 ==================
 */
static void
CG_PlayerHealthBar(centity_t *cent)
{
  vec3_t origin, origin2, down, right, back, downLength, rightLength;
  float rimWidth = HEALTH_BAR_HEIGHT / 15.0f;
  float doneWidth, leftWidth, progress;
  int health;
  qhandle_t shader;
  entityState_t *es;
  vec3_t mins, maxs;

  if (cgs.survivalRecords[0] < 1)
  {
    return;
  }

  if (cgs.clientinfo[cent->currentState.number].team == PTE_ALIENS)
    return;

  es = &cent->currentState;

  health = es->constantLight;

  //cgs.clientinfo[ cent->currentState.number ].team==PTE_ALIENS
  //health =  cg.snap->ps.stats[STAT_HEALTH];
  //health = cgs.clientinfo[cg.crosshairClientNum].health;

  progress = (float) health / 100; // / B_HEALTH_SCALE;

  if (progress < 0.0f)
    progress = 0.0f;
  else if (progress > 1.0f)
    progress = 1.0f;

  if (progress < 0.33f)
    shader = cgs.media.redBuildShader;
  else
    shader = cgs.media.greenBuildShader;

  doneWidth = (HEALTH_BAR_WIDTH - 2 * rimWidth) * progress;
  leftWidth = (HEALTH_BAR_WIDTH - 2 * rimWidth) - doneWidth;

  VectorCopy( cg.refdef.viewaxis[ 2 ], down );
  VectorInverse(down);
  VectorCopy( cg.refdef.viewaxis[ 1 ], right );
  VectorInverse(right);
  VectorSubtract( cg.refdef.vieworg, cent->lerpOrigin, back );
  VectorNormalize(back);
  VectorCopy( cent->lerpOrigin, origin );
  origin[2] += 40;

  BG_FindBBoxForClass(PCL_HUMAN, mins, maxs, NULL, NULL, NULL);
  //BG_FindBBoxForBuildable( es->modelindex, mins, maxs );
  VectorMA( origin, 48.0f, es->origin2, origin );
  VectorMA( origin, -HEALTH_BAR_WIDTH / 2.0f, right, origin );
  VectorMA( origin, maxs[ 0 ] + 8.0f, back, origin );

  VectorCopy( origin, origin2 );
  VectorScale( right, rimWidth + doneWidth, rightLength );
  VectorScale( down, HEALTH_BAR_HEIGHT, downLength );
  CG_DrawPlane(origin2, downLength, rightLength, shader);

  VectorMA( origin, rimWidth + doneWidth, right, origin2 );
  VectorScale( right, leftWidth, rightLength );
  VectorScale( down, rimWidth, downLength );
  CG_DrawPlane(origin2, downLength, rightLength, shader);

  VectorMA( origin, rimWidth + doneWidth, right, origin2 );
  VectorMA( origin2, HEALTH_BAR_HEIGHT - rimWidth, down, origin2 );
  VectorScale( right, leftWidth, rightLength );
  VectorScale( down, rimWidth, downLength );
  CG_DrawPlane(origin2, downLength, rightLength, shader);

  VectorMA( origin, HEALTH_BAR_WIDTH - rimWidth, right, origin2 );
  VectorScale( right, rimWidth, rightLength );
  VectorScale( down, HEALTH_BAR_HEIGHT, downLength );
  CG_DrawPlane(origin2, downLength, rightLength, shader);

  /*if( !( es->generic1 & B_POWERED_TOGGLEBIT ) &&
   BG_FindTeamForBuildable( es->modelindex ) == BIT_HUMANS )
   {*/
  VectorMA( origin, 15.0f, right, origin2 );
  VectorMA( origin2, HEALTH_BAR_HEIGHT + 5.0f, down, origin2 );
  VectorScale( right, HEALTH_BAR_WIDTH / 2.0f - 5.0f, rightLength );
  VectorScale( down, HEALTH_BAR_WIDTH / 2.0f - 5.0f, downLength );
  //CG_DrawPlane( origin2, downLength, rightLength, cgs.media.noPowerShader );
  /*}*/
}

/*
 ===============
 CG_Player
 ===============
 */
void
CG_Player(centity_t *cent)
{
  clientInfo_t *ci;

  //TA: NOTE: legs is used for nonsegmented models
  //          this helps reduce code to be changed
  refEntity_t legs;
  refEntity_t torso;
  refEntity_t head;
  int clientNum;
  int renderfx;
  qboolean shadow = qfalse;
  float shadowPlane;
  entityState_t *es = &cent->currentState;
  pClass_t class = (es->powerups >> 8) & 0xFF;
  float scale;
  vec3_t tempAxis[3], tempAxis2[3];
  vec3_t angles;
  int held = es->modelindex;
  vec3_t surfNormal =
  { 0.0f, 0.0f, 1.0f };

  // the client number is stored in clientNum.  It can't be derived
  // from the entity number, because a single client may have
  // multiple corpses on the level using the same clientinfo
  clientNum = es->clientNum;
  if (clientNum < 0 || clientNum >= MAX_CLIENTS)
    CG_Error("Bad clientNum on player entity");

  ci = &cgs.clientinfo[clientNum];

  // it is possible to see corpses from disconnected players that may
  // not have valid clientinfo
  if (!ci->infoValid)
    return;

  //don't draw
  if (es->eFlags & EF_NODRAW)
    return;

  // get the player model information
  renderfx = 0;
  if (es->number == cg.snap->ps.clientNum)
  {
    if (!cg.renderingThirdPerson)
      renderfx = RF_THIRD_PERSON; // only draw in mirrors
    else if (cg_cameraMode.integer)
      return;
  }

  if (es->eFlags & EF_ONFIRE)
  {
    CG_PlayerOnFire(cent);
  }

  if (cg_drawBBOX.integer)
  {
    vec3_t mins, maxs;

    BG_FindBBoxForClass(class, mins, maxs, NULL, NULL, NULL);
    CG_DrawBoundingBox(cent->lerpOrigin, mins, maxs);
  }

  CG_PlayerHealthBar(cent);

  memset(&legs, 0, sizeof(legs));
  memset(&torso, 0, sizeof(torso));
  memset(&head, 0, sizeof(head));

  VectorCopy( cent->lerpAngles, angles );
  AnglesToAxis(cent->lerpAngles, tempAxis);

  //rotate lerpAngles to floor
  if (es->eFlags & EF_WALLCLIMB && BG_RotateAxis(es->angles2, tempAxis, tempAxis2, qtrue, es->eFlags & EF_WALLCLIMBCEILING))
    AxisToAngles(tempAxis2, angles);
else    VectorCopy( cent->lerpAngles, angles );

    //normalise the pitch
    if( angles[ PITCH ] < -180.0f )
    angles[ PITCH ] += 360.0f;

    // get the rotation information
    if( !ci->nonsegmented )
    CG_PlayerAngles( cent, angles, legs.axis, torso.axis, head.axis );
    else
    CG_PlayerNonSegAngles( cent, angles, legs.axis );

    AxisCopy( legs.axis, tempAxis );

    //rotate the legs axis to back to the wall
    if( es->eFlags & EF_WALLCLIMB &&
        BG_RotateAxis( es->angles2, legs.axis, tempAxis, qfalse, es->eFlags & EF_WALLCLIMBCEILING ) )
    AxisCopy( tempAxis, legs.axis );

    //smooth out WW transitions so the model doesn't hop around
    CG_PlayerWWSmoothing( cent, legs.axis, legs.axis );

    AxisCopy( tempAxis, cent->pe.lastAxis );

    // get the animation state (after rotation, to allow feet shuffle)
    if( !ci->nonsegmented )
    CG_PlayerAnimation( cent, &legs.oldframe, &legs.frame, &legs.backlerp,
        &torso.oldframe, &torso.frame, &torso.backlerp );
    else
    CG_PlayerNonSegAnimation( cent, &legs.oldframe, &legs.frame, &legs.backlerp );

    // add the talk baloon or disconnect icon
    CG_PlayerSprites( cent );

    // add the shadow
    if( ( es->number == cg.snap->ps.clientNum && cg.renderingThirdPerson ) ||
        es->number != cg.snap->ps.clientNum )
    shadow = CG_PlayerShadow( cent, &shadowPlane, class );

    // add a water splash if partially in and out of water
    CG_PlayerSplash( cent, class );

    if( cg_shadows.integer == 3 && shadow )
    renderfx |= RF_SHADOW_PLANE;

    renderfx |= RF_LIGHTING_ORIGIN; // use the same origin for all

    //
    // add the legs
    //
    if( !ci->nonsegmented )
    {
      legs.hModel = ci->legsModel;

      if( held & ( 1 << UP_LIGHTARMOUR ) )
      legs.customSkin = cgs.media.larmourLegsSkin;
      else
      legs.customSkin = ci->legsSkin;

      if(cgs.clientinfo[ cent->currentState.number ].team==PTE_ALIENS)
      {
        legs.customSkin = ci->zombielegsSkin;
      }
    }
    else
    {
      legs.hModel = ci->nonSegModel;
      legs.customSkin = ci->nonSegSkin;
    }

    VectorCopy( cent->lerpOrigin, legs.origin );

    VectorCopy( cent->lerpOrigin, legs.lightingOrigin );
    legs.shadowPlane = shadowPlane;
    legs.renderfx = renderfx;
    VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all

    //move the origin closer into the wall with a CapTrace
    if( es->eFlags & EF_WALLCLIMB && !( es->eFlags & EF_DEAD ) && !( cg.intermissionStarted ) )
    {
      vec3_t start, end, mins, maxs;
      trace_t tr;

      if( es->eFlags & EF_WALLCLIMBCEILING )
      VectorSet( surfNormal, 0.0f, 0.0f, -1.0f );
      else
      VectorCopy( es->angles2, surfNormal );

      BG_FindBBoxForClass( class, mins, maxs, NULL, NULL, NULL );

      VectorMA( legs.origin, -TRACE_DEPTH, surfNormal, end );
      VectorMA( legs.origin, 1.0f, surfNormal, start );
      CG_CapTrace( &tr, start, mins, maxs, end, es->number, MASK_PLAYERSOLID );

      //if the trace misses completely then just use legs.origin
      //apparently capsule traces are "smaller" than box traces
      if( tr.fraction != 1.0f )
      VectorMA( legs.origin, tr.fraction * -TRACE_DEPTH, surfNormal, legs.origin );

      VectorCopy( legs.origin, legs.lightingOrigin );
      VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all
    }

    //rescale the model
    scale = BG_FindModelScaleForClass( class );

    if( scale != 1.0f )
    {
      VectorScale( legs.axis[ 0 ], scale, legs.axis[ 0 ] );
      VectorScale( legs.axis[ 1 ], scale, legs.axis[ 1 ] );
      VectorScale( legs.axis[ 2 ], scale, legs.axis[ 2 ] );

      legs.nonNormalizedAxes = qtrue;
    }

    //offset on the Z axis if required
    VectorMA( legs.origin, BG_FindZOffsetForClass( class ), surfNormal, legs.origin );
    VectorCopy( legs.origin, legs.lightingOrigin );
    VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all

    trap_R_AddRefEntityToScene( &legs );

    // if the model failed, allow the default nullmodel to be displayed
    if( !legs.hModel )
    return;

    if( !ci->nonsegmented )
    {
      //
      // add the torso
      //
      torso.hModel = ci->torsoModel;

      if( held & ( 1 << UP_LIGHTARMOUR ) )
      torso.customSkin = cgs.media.larmourTorsoSkin;
      else
      torso.customSkin = ci->torsoSkin;

      if(cgs.clientinfo[ cent->currentState.number ].team==PTE_ALIENS)
      {
        torso.customSkin = ci->zombietorsoSkin;
      }

      if( !torso.hModel )
      return;

      VectorCopy( cent->lerpOrigin, torso.lightingOrigin );

      CG_PositionRotatedEntityOnTag( &torso, &legs, ci->legsModel, "tag_torso" );
      /*
       torso.shaderRGBA[0] = ci->color1[0]*255;//add
       torso.shaderRGBA[1] = ci->color1[1]*255;//add
       torso.shaderRGBA[2] = ci->color1[2]*255;//add
       torso.shaderRGBA[3] = 255;
       */

      if(cgs.clientinfo[ cent->currentState.number ].team==PTE_ALIENS)
      {
        torso.shaderRGBA[0] = 1*255;//add
        torso.shaderRGBA[1] = 0*255;//add
        torso.shaderRGBA[2] = 0*255;//add
      }
      if(cgs.clientinfo[ cent->currentState.number ].team==PTE_HUMANS)
      {
        torso.shaderRGBA[0] = 0*255;//add
        torso.shaderRGBA[1] = 1*255;//add
        torso.shaderRGBA[2] = 1*255;//add
      }

      torso.shaderRGBA[3] = 255;

      torso.shadowPlane = shadowPlane;
      torso.renderfx = renderfx;

      trap_R_AddRefEntityToScene( &torso );

      //
      // add the head
      //
      head.hModel = ci->headModel;

      if( held & ( 1 << UP_HELMET ) )
      head.customSkin = cgs.media.larmourHeadSkin;
      else
      head.customSkin = ci->headSkin;

      if(cgs.clientinfo[ cent->currentState.number ].team==PTE_ALIENS)
      {
        head.customSkin = ci->zombieheadSkin;
      }

      if( !head.hModel )
      return;

      VectorCopy( cent->lerpOrigin, head.lightingOrigin );

      CG_PositionRotatedEntityOnTag( &head, &torso, ci->torsoModel, "tag_head" );

      if(cgs.clientinfo[ cent->currentState.number ].team==PTE_ALIENS)
      {
        head.shaderRGBA[0] = 1*255;//add
        head.shaderRGBA[1] = 0*255;//add
        head.shaderRGBA[2] = 0*255;//add
      }
      if(cgs.clientinfo[ cent->currentState.number ].team==PTE_HUMANS)
      {
        head.shaderRGBA[0] = 0*255;//add
        head.shaderRGBA[1] = 0*255;//add
        head.shaderRGBA[2] = 1*255;//add
      }

      head.shaderRGBA[3] = 255;

      head.shadowPlane = shadowPlane;
      head.renderfx = renderfx;

      trap_R_AddRefEntityToScene( &head );
    }

    //
    // add the gun / barrel / flash
    //
    if( es->weapon != WP_NONE )
    {
      if( !ci->nonsegmented )
      CG_AddPlayerWeapon( &torso, NULL, cent );
      else
      CG_AddPlayerWeapon( &legs, NULL, cent );
    }

    CG_PlayerUpgrades( cent, &torso );

    //sanity check that particle systems are stopped when dead
    if( es->eFlags & EF_DEAD )
    {
      if( CG_IsParticleSystemValid( &cent->muzzlePS ) )
      CG_DestroyParticleSystem( &cent->muzzlePS );

      if( CG_IsParticleSystemValid( &cent->jetPackPS ) )
      CG_DestroyParticleSystem( &cent->jetPackPS );
    }

    VectorCopy( surfNormal, cent->pe.lastNormal );
  }

  /*
   ===============
   CG_Corpse
   ===============
   */
void
CG_Corpse(centity_t *cent)
{
  clientInfo_t *ci;
  refEntity_t legs;
  refEntity_t torso;
  refEntity_t head;
  entityState_t *es = &cent->currentState;
  int corpseNum;
  int renderfx;
  qboolean shadow = qfalse;
  float shadowPlane;
  vec3_t origin, liveZ, deadZ;
  float scale;

  corpseNum = CG_GetCorpseNum(es->clientNum);

  if (corpseNum < 0 || corpseNum >= MAX_CLIENTS)
    CG_Error("Bad corpseNum on corpse entity: %d", corpseNum);

  ci = &cgs.corpseinfo[corpseNum];

  // it is possible to see corpses from disconnected players that may
  // not have valid clientinfo
  if (!ci->infoValid)
    return;

  memset(&legs, 0, sizeof(legs));
  memset(&torso, 0, sizeof(torso));
  memset(&head, 0, sizeof(head));

  VectorCopy( cent->lerpOrigin, origin );
  BG_FindBBoxForClass(es->clientNum, liveZ, NULL, NULL, deadZ, NULL);
  origin[2] -= (liveZ[2] - deadZ[2]);

  VectorCopy( es->angles, cent->lerpAngles );

  // get the rotation information
  if (!ci->nonsegmented)
    CG_PlayerAngles(cent, cent->lerpAngles, legs.axis, torso.axis, head.axis);
  else
    CG_PlayerNonSegAngles(cent, cent->lerpAngles, legs.axis);

  //set the correct frame (should always be dead)
  if (cg_noPlayerAnims.integer)
    legs.oldframe = legs.frame = torso.oldframe = torso.frame = 0;
  else if (!ci->nonsegmented)
  {
    memset(&cent->pe.legs, 0, sizeof(lerpFrame_t));
    CG_RunPlayerLerpFrame(ci, &cent->pe.legs, es->legsAnim, 1);
    legs.oldframe = cent->pe.legs.oldFrame;
    legs.frame = cent->pe.legs.frame;
    legs.backlerp = cent->pe.legs.backlerp;

    memset(&cent->pe.torso, 0, sizeof(lerpFrame_t));
    CG_RunPlayerLerpFrame(ci, &cent->pe.torso, es->torsoAnim, 1);
    torso.oldframe = cent->pe.torso.oldFrame;
    torso.frame = cent->pe.torso.frame;
    torso.backlerp = cent->pe.torso.backlerp;
  }
  else
  {
    memset(&cent->pe.nonseg, 0, sizeof(lerpFrame_t));
    CG_RunPlayerLerpFrame(ci, &cent->pe.nonseg, es->legsAnim, 1);
    legs.oldframe = cent->pe.nonseg.oldFrame;
    legs.frame = cent->pe.nonseg.frame;
    legs.backlerp = cent->pe.nonseg.backlerp;
  }

  // add the shadow
  shadow = CG_PlayerShadow(cent, &shadowPlane, es->clientNum);

  // get the player model information
  renderfx = 0;

  if (cg_shadows.integer == 3 && shadow)
    renderfx |= RF_SHADOW_PLANE;

  renderfx |= RF_LIGHTING_ORIGIN; // use the same origin for all

  //
  // add the legs
  //
  if (!ci->nonsegmented)
  {
    legs.hModel = ci->legsModel;
    legs.customSkin = ci->legsSkin;
  }
  else
  {
    legs.hModel = ci->nonSegModel;
    legs.customSkin = ci->nonSegSkin;
  }

  VectorCopy( origin, legs.origin );

  VectorCopy( origin, legs.lightingOrigin );
  legs.shadowPlane = shadowPlane;
  legs.renderfx = renderfx;
  legs.origin[2] += BG_FindZOffsetForClass(es->clientNum);
  VectorCopy( legs.origin, legs.oldorigin ); // don't positionally lerp at all

  //rescale the model
  scale = BG_FindModelScaleForClass(es->clientNum);

  if (scale != 1.0f)
  {
    VectorScale( legs.axis[ 0 ], scale, legs.axis[ 0 ] );
    VectorScale( legs.axis[ 1 ], scale, legs.axis[ 1 ] );
    VectorScale( legs.axis[ 2 ], scale, legs.axis[ 2 ] );

    legs.nonNormalizedAxes = qtrue;
  }

  //CG_AddRefEntityWithPowerups( &legs, es->powerups, ci->team );
  trap_R_AddRefEntityToScene(&legs);

  // if the model failed, allow the default nullmodel to be displayed
  if (!legs.hModel)
    return;

  if (!ci->nonsegmented)
  {
    //
    // add the torso
    //
    torso.hModel = ci->torsoModel;
    if (!torso.hModel)
      return;

    torso.customSkin = ci->torsoSkin;

    VectorCopy( origin, torso.lightingOrigin );

    CG_PositionRotatedEntityOnTag(&torso, &legs, ci->legsModel, "tag_torso");

    torso.shadowPlane = shadowPlane;
    torso.renderfx = renderfx;

    //CG_AddRefEntityWithPowerups( &torso, es->powerups, ci->team );
    trap_R_AddRefEntityToScene(&torso);

    //
    // add the head
    //
    head.hModel = ci->headModel;
    if (!head.hModel)
      return;

    head.customSkin = ci->headSkin;

    VectorCopy( origin, head.lightingOrigin );

    CG_PositionRotatedEntityOnTag(&head, &torso, ci->torsoModel, "tag_head");

    head.shadowPlane = shadowPlane;
    head.renderfx = renderfx;

    //CG_AddRefEntityWithPowerups( &head, es->powerups, ci->team );
    trap_R_AddRefEntityToScene(&head);
  }
}

//=====================================================================

/*
 ===============
 CG_ResetPlayerEntity

 A player just came into view or teleported, so reset all animation info
 ===============
 */
void
CG_ResetPlayerEntity(centity_t *cent)
{
  cent->errorTime = -99999; // guarantee no error decay added
  cent->extrapolated = qfalse;

  CG_ClearLerpFrame(&cgs.clientinfo[cent->currentState.clientNum], &cent->pe.legs, cent->currentState.legsAnim);
  CG_ClearLerpFrame(&cgs.clientinfo[cent->currentState.clientNum], &cent->pe.torso, cent->currentState.torsoAnim);
  CG_ClearLerpFrame(&cgs.clientinfo[cent->currentState.clientNum], &cent->pe.nonseg, cent->currentState.legsAnim);

  BG_EvaluateTrajectory(&cent->currentState.pos, cg.time, cent->lerpOrigin);
  BG_EvaluateTrajectory(&cent->currentState.apos, cg.time, cent->lerpAngles);

  VectorCopy( cent->lerpOrigin, cent->rawOrigin );
  VectorCopy( cent->lerpAngles, cent->rawAngles );

  memset(&cent->pe.legs, 0, sizeof(cent->pe.legs));
  cent->pe.legs.yawAngle = cent->rawAngles[YAW];
  cent->pe.legs.yawing = qfalse;
  cent->pe.legs.pitchAngle = 0;
  cent->pe.legs.pitching = qfalse;

  memset(&cent->pe.torso, 0, sizeof(cent->pe.legs));
  cent->pe.torso.yawAngle = cent->rawAngles[YAW];
  cent->pe.torso.yawing = qfalse;
  cent->pe.torso.pitchAngle = cent->rawAngles[PITCH];
  cent->pe.torso.pitching = qfalse;

  memset(&cent->pe.nonseg, 0, sizeof(cent->pe.nonseg));
  cent->pe.nonseg.yawAngle = cent->rawAngles[YAW];
  cent->pe.nonseg.yawing = qfalse;
  cent->pe.nonseg.pitchAngle = cent->rawAngles[PITCH];
  cent->pe.nonseg.pitching = qfalse;

  if (cg_debugPosition.integer)
    CG_Printf("%i ResetPlayerEntity yaw=%i\n", cent->currentState.number, cent->pe.torso.yawAngle);
}

/*
 ==================
 CG_PlayerDisconnect

 Player disconnecting
 ==================
 */
void
CG_PlayerDisconnect(vec3_t org)
{
  particleSystem_t *ps;

  trap_S_StartSound(org, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.disconnectSound);

  ps = CG_SpawnNewParticleSystem(cgs.media.disconnectPS);

  if (CG_IsParticleSystemValid(&ps))
  {
    CG_SetAttachmentPoint(&ps->attachment, org);
    CG_AttachToPoint(&ps->attachment);
  }
}

/*
 =================
 CG_Bleed

 This is the spurt of blood when a character gets hit
 =================
 */
void
CG_Bleed(vec3_t origin, vec3_t normal, int entityNum)
{
  pTeam_t team = cgs.clientinfo[entityNum].team;
  qhandle_t bleedPS;
  particleSystem_t *ps;

  if (!cg_blood.integer)
    return;

  if (team == PTE_ALIENS)
    bleedPS = cgs.media.alienBleedPS;
  else if (team == PTE_HUMANS)
    bleedPS = cgs.media.humanBleedPS;
  else
    return;

  ps = CG_SpawnNewParticleSystem(bleedPS);

  if (CG_IsParticleSystemValid(&ps))
  {
    CG_SetAttachmentPoint(&ps->attachment, origin);
    CG_SetAttachmentCent(&ps->attachment, &cg_entities[entityNum]);
    CG_AttachToPoint(&ps->attachment);

    CG_SetParticleSystemNormal(ps, normal);
  }
}

/*
 ===============
 CG_AtHighestClass

 Is the local client at the highest class possible?
 ===============
 */
qboolean
CG_AtHighestClass(void)
{
  int i;
  qboolean superiorClasses = qfalse;

  for(i = PCL_NONE + 1;i < PCL_NUM_CLASSES;i++)
  {
    if (BG_ClassCanEvolveFromTo(cg.predictedPlayerState.stats[STAT_PCLASS], i, ALIEN_MAX_KILLS, 0) >= 0 && BG_FindStagesForClass(i, cgs.alienStage)
        && BG_ClassIsAllowed(i))
    {
      superiorClasses = qtrue;
      break;
    }
  }

  return !superiorClasses;
}

//Flamer fire effect :).
void CG_PlayerOnFire(centity_t *cent)
{
  particleSystem_t *ps;

  if(cent->fireTime > cg.time)
  {
    return;
  }
  else
  {
    cent->fireTime = cg.time + 3000;
  }

  trap_S_StartSound(cent->lerpOrigin, ENTITYNUM_WORLD, CHAN_AUTO, cgs.media.fireSound);

  ps = CG_SpawnNewParticleSystem(cgs.media.fire);

  if (CG_IsParticleSystemValid(&ps))
  {
    CG_SetAttachmentPoint(&ps->attachment, cent->lerpOrigin);
    CG_AttachToPoint(&ps->attachment);
  }
}

